
/*    This file is part of the Managarm operating system.
 *   Copyright (C) 2007, 2008, 2009  Alexander van der Grinten
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>. */

package org.managarm.jmanacclib.frontend.korona;

import java.util.Stack;
import org.managarm.jmanacclib.core.CclibReader;
import org.managarm.jmanacclib.core.CclibWriter;
import org.managarm.jmanacclib.core.CclibSource;
import org.managarm.jmanacclib.core.CclibUnit;
import org.managarm.jmanacclib.core.CclibException;

public final class KorPreprocessor {
	// current environment object
	private final KorEnvironment environment;
	// the input/output streams this preprocessor uses
	private final CclibReader reader;
	private final CclibWriter writer;
	private final Stack<Character> stack = new Stack<Character>();
	// current line
	private int line;
	// our identifier tree used to parse identifiers
	private KorIdentTree localIdents = new KorIdentTree();
	
	public KorPreprocessor(final KorEnvironment environment,
			final CclibReader reader, final CclibWriter writer) {
		this.environment = environment;
		this.reader = reader;
		this.writer = writer;
	}
	
	public void preprocess() throws CclibException {
		writer.locate(reader.getSource());
		while(endReached() == false) {
			parseStatement();
		}
	}
	
	private void parseStatement() throws CclibException {
		// use identifier trees to parse macros and substitutions
		Object replacement = parseTree(localIdents);
		if(replacement == null) // parse global identifiers
			replacement = parseTree(environment.prepIdentifiers);
		
		if(replacement instanceof KorEnvironment.PrepSubstitution) {
			final KorEnvironment.PrepSubstitution subs
					= (KorEnvironment.PrepSubstitution)replacement;
			writer.emit(subs.replace);
			relocate();
		}else if(replacement instanceof KorEnvironment.PrepMacro) {
			final KorEnvironment.PrepMacro macro
					= (KorEnvironment.PrepMacro)replacement;
			String macrocode = macro.code;
			if(macro.extended) {
				for(int i = 0; i < macro.arguments; i++) {
					skipSpace(false);
					final String argument = readBlock();
					
					// replace the argument
					macrocode = macrocode.replace("$[" + i + "]", argument);
				}
			}else{
				if(matchString("$(") == false)
					throw new CclibException("Illegal use of macro "
							+ macro.identifier,
							getCurrentUnit(), getCurrentLine());
				
				// match the closing $) if the macro has no arguments
				if(macro.arguments == 0)
					if(matchString("$)") == false)
						throw new CclibException("Macro " + macro.identifier
								+ " does not accept arguments");
				
				// parse the arguments
				for(int i = 1; i <= macro.arguments; i++) {
					final StringBuffer argument = new StringBuffer();
					while(true) {
						if(matchString("$|")) {
							if(i == macro.arguments)
								throw new CclibException("Too many arguments for macro "
										+ macro.identifier,
										getCurrentUnit(), getCurrentLine());
							break;
						}else if(matchString("$)")) {
							if(i < macro.arguments)
								throw new CclibException("Not enough arguments for macro "
										+ macro.identifier,
										getCurrentUnit(), getCurrentLine());
							break;
						}else argument.append(popChar());
					}
					
					// do the argument substitution
					macrocode = macrocode.replace("$[" + i + "]",
							argument.toString());
				}
			}
			
			final CclibUnit unit = cloneUnit(macro.source.unit,
					getCurrentUnit(), getCurrentLine());
			final CclibSource macrsource = new CclibSource(unit,
					macro.source.line);
			final KorPreprocessor macrproc = new KorPreprocessor(environment,
					new CclibReader.StringReader(macrsource, macrocode), writer);
			macrproc.preprocess();
			relocate();
		}else if(matchString("$include")) {
			skipSpace(false);
			final String filename = readBlock();
			
			final CclibReader reader = environment.openFile
					(environment.findUnit(getCurrentUnit(),
							getCurrentLine(), filename));
			final KorPreprocessor preprocessor
					= new KorPreprocessor(environment, reader, writer);
			preprocessor.preprocess();
			relocate();
		}else if(matchString("$definex")) {
			skipSpace(false);
			final String defname = readBlock();
			
			environment.prepDefinitions.add(defname);
		}else if(matchString("$define")) {
			skipSpace(true);
			final String defname = parseIdent();
			
			environment.prepDefinitions.add(defname);
		}else if(matchString("$substitute")) {
			skipSpace(false);
			final String subname = readBlock();
			skipSpace(false);
			final String subtext = readBlock();
			relocate();

			environment.prepIdentifiers.insert(subname,
					new KorEnvironment.PrepSubstitution(subname, subtext),
					getCurrentUnit(), getCurrentLine());
		}else if(matchString("$locsubstitute")) {
			skipSpace(false);
			final String subname = readBlock();
			skipSpace(false);
			final String subtext = readBlock();
			relocate();
			
			localIdents.insert(subname,
					new KorEnvironment.PrepSubstitution(subname, subtext),
					getCurrentUnit(), getCurrentLine());
		}else if(matchString("$macro")) {
			skipSpace(true);
			final String ident = parseIdent();
			skipSpace(true);
			final int args = parseInteger();
			skipSpace(false);
			final String macro = readBlock();
			relocate();
			
			final CclibSource macrsource = new CclibSource(getCurrentUnit(),
					getCurrentLine());
			environment.prepIdentifiers.insert(ident,
					new KorEnvironment.PrepMacro (macrsource, ident,
					args, macro, false), getCurrentUnit(), getCurrentLine());
		}else if(matchString("$xmacro")) {
			skipSpace(true);
			final String ident = parseIdent();
			skipSpace(true);
			final int args = parseInteger();
			skipSpace(false);
			final String macro = readBlock();
			relocate();
			
			final CclibSource macrsource = new CclibSource(getCurrentUnit(),
					getCurrentLine());
			environment.prepIdentifiers.insert(ident,
					new KorEnvironment.PrepMacro (macrsource, ident,
					args, macro, true), getCurrentUnit(), getCurrentLine());
		}else if(matchString("$template")) {
			skipSpace(true);
			final String ident = parseIdent();
			skipSpace(false);
			final String block = readBlock();
			relocate();
			
			final String macro = "$ifndefx $<--TMPL_DEFINED_" + ident + "_$[1]$--> "
					+ "$<-- $definex $<--TMPL_DEFINED_" + ident + "_$[1]$-->"
					+ block + "$-->";
			
			final CclibSource macrsource = new CclibSource(getCurrentUnit(),
					getCurrentLine());
			environment.prepIdentifiers.insert("template_" + ident,
					new KorEnvironment.PrepMacro(macrsource,
					"template_" + ident, 1, macro, false),
					getCurrentUnit(), getCurrentLine());
		}else if(matchString("$xtemplate")) {
			skipSpace(true);
			final String ident = parseIdent();
			skipSpace(false);
			final String block = readBlock();
			relocate();
			
			final String macro = "$ifndefx $<--TMPL_DEFINED_" + ident + "_$[1]$--> "
					+ "$<-- $definex $<--TMPL_DEFINED_" + ident + "_$[1]$-->"
					+ block + "$-->";
			
			final CclibSource macrsource = new CclibSource(getCurrentUnit(),
					getCurrentLine());
			environment.prepIdentifiers.insert("template_" + ident,
					new KorEnvironment.PrepMacro(macrsource,
					"template_" + ident, 1, macro, true),
					getCurrentUnit(), getCurrentLine());
		}else if(matchString("$forget")) {
			skipSpace(false);
			final String ident = readBlock();

			localIdents.remove(ident);
			environment.prepIdentifiers.remove(ident);
		}else if(matchString("$ifdef")) {
			skipSpace(true);
			final String defname = parseIdent();
			skipSpace(false);
			parseIf(environment.prepIsDef(defname));
		}else if(matchString("$ifndefx")) {
			skipSpace(false);
			final String defname = readBlock();
			skipSpace(false);
			parseIf(!environment.prepIsDef(defname));
		}else if(matchString("$ifndef")) {
			skipSpace(true);
			final String defname = parseIdent();
			skipSpace(false);
			parseIf(!environment.prepIsDef(defname));
		}else{
			processChar(popChar(), true); // read a single character
		}
	}
	

	public Object parseTree(KorIdentTree tree) throws CclibException {
		final Character c = popChar();
		
		// there is no entry matching the identifier
		if(tree.containsChild(c) == false) {
			pushChar(c);
			return tree.getObject();
		}
		
		// continue parsing recursively
		final Object child = parseTree(tree.getChild(c));
		if(child == null)
			pushChar(c);
		return child;
	}
	
	// parses if-statements
	private void parseIf(boolean condition) throws CclibException {
		if(condition) {
			forceBlockHead();
			while(consumeBlockTail() == false) {
				parseStatement();
			}
		}else{
			skipBlock();
		}
		
		boolean success = condition;
		while(endReached() == false) {
			boolean elsecond; // parse else-statements
			if(matchString("$elseifdef")) {
				skipSpace(true);
				final String defname = parseIdent();
				skipSpace(false);
				elsecond = environment.prepIsDef(defname);
			}else if(matchString("$elseifdef")) {
				skipSpace(true);
				final String defname = parseIdent();
				skipSpace(false);
				elsecond = !environment.prepIsDef(defname);
			}else if(matchString("$else")) {
				skipSpace(false);
				elsecond = true;
			}else break;
			
			if(success || elsecond == false) {
				skipBlock(); // one of the upper cases triggered
			}else{
				forceBlockHead();
				while(consumeBlockTail() == false) {
					parseStatement();
				}
				success = true;
			}
		}
	}
	
	private void forceBlockHead() throws CclibException {
		if(matchString("$<--") == false)
			throw new CclibException("Expected a preprocessor block",
					getCurrentUnit(), getCurrentLine());
	}
	
	private boolean consumeBlockTail() throws CclibException {
		return matchString("$-->");
	}
	
	private String readBlock() throws CclibException {
		forceBlockHead();
		
		final StringBuffer string = new StringBuffer();
		int blockcnt = 1;
		while(blockcnt != 0) {
			if(matchString("$<--")) {
				string.append("$<--");
				blockcnt++;
			}else if(matchString("$-->")) {
				if(blockcnt != 1)
					string.append("$-->");
				blockcnt--;
			}else{
				string.append(popChar());
			}
		}
		return string.toString();
	}
	
	private void skipBlock() throws CclibException {
		forceBlockHead();
		
		int blockcnt = 1;
		while(blockcnt != 0) {
			if(matchString("$<--")) {
				blockcnt++;
			}else if(matchString("$-->")) {
				blockcnt--;
			}else{
				processChar(popChar(), false); // skip a single character
			}
		}
	}
	
	private void skipSpace(final boolean required) throws CclibException {
		boolean matched = false;
		while(endReached() == false) {
			final char c = popChar();
			if(c == ' ' || c == '\t' || c == '\n') {
				processChar(c, false);
				matched = true;
			}else {
				pushChar(c);
				break; // we reached the end of the whitespace
			}
		}
		if(required && matched == false)
			throw new CclibException("Expected whitespace",
					getCurrentUnit(), getCurrentLine());
	}
	
	private void processChar(final char c,
			final boolean print) throws CclibException {
		if(c == '\n')
			line++; // increment the line counter
		if(print == true)
			writer.emit(c);
	}
	
	private boolean matchString(final String string) throws CclibException {
		for(int i = 0; i < string.length(); i++) {
			if(endReached() == true) {
				// put all chars back into the stream
				for(int j = i - 1; j >= 0; j--)
					pushChar(string.charAt(j));
				return false;
			}
			
			final char c = popChar();
			if(c != string.charAt(i)) {
				pushChar(c);
				// put all chars back into the stream
				for(int j = i - 1; j >= 0; j--)
					pushChar(string.charAt(j));
				return false;
			}
		}
		return true;
	}
	
	private String parseIdent() throws CclibException {
		final StringBuffer buf = new StringBuffer();
		while(endReached() == false) {
			final char c = popChar();
			if((c < 'a' || c > 'z')
					&& (c < 'A' || c > 'Z')
					&& c != '_') {
				pushChar(c);
				break;
			}else buf.append(c);
		}
		return buf.toString();
	}
	
	private int parseInteger() throws CclibException {
		final StringBuffer buf = new StringBuffer();
		while(endReached() == false) {
			final char c = popChar();
			if(c < '0' || c > '9') {
				pushChar(c);
				break;
			}else buf.append(c);
		}
		return Integer.parseInt(buf.toString());
	}
	
	private char popChar() throws CclibException {
		if(stack.empty() == false)
			return stack.pop();
		return reader.fetch();
	}
	
	private void pushChar(char c) throws CclibException {
		stack.push(c);
	}
	
	private boolean endReached() throws CclibException {
		if(stack.empty() == false)
			return false;
		return reader.endReached();
	}

	private int getCurrentLine() throws CclibException {
		int disp = 0;
		for(int i = 0; i < stack.size(); i++)
			if(stack.get(i) == '\n')
				disp--;
		return reader.getLine() + disp;
	}
	private CclibUnit getCurrentUnit() throws CclibException {
		return reader.getSource().unit;
	}
	private void relocate() throws CclibException {
		writer.locate(new CclibSource(getCurrentUnit(), getCurrentLine()));
	}
	
	private CclibUnit cloneUnit(CclibUnit original,
			CclibUnit tailunit, int tailline) {
		if(original.includeLine == -1) {
			return new CclibUnit(tailunit, tailline, original.unitPath);
		}else{
			return new CclibUnit(cloneUnit(original.includeUnit,
					tailunit, tailline), original.includeLine, original.unitPath);
		}
	}
}
